package com.xzzz.common.wechat.security;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidParameterSpecException;
import java.util.Arrays;

/**
 * 对称解密使用的算法为 AES-128-CBC，数据采用PKCS#7填充。
 *
 * 1. AES-128-CBC是一种分组对称加密算法，即用同一组key进行明文和密文的转换，
 *    以128bit为一组，128bit==16Byte，意思就是明文的16字节为一组对应加密后的16字节的密文。
 *
 * 2. 若最后剩余的明文不够16字节，需要进行填充，通常采用PKCS7进行填充。比如最后缺3个字节，
 *    则填充3个字节的0x03;若最后缺10个字节，则填充10个字节的0x0a;
 *
 * 3. 若明文正好是16个字节的整数倍，最后要再加入一个16字节0x10的组再进行加密
 *
 * @author xzzz
 * 
 * @since 1.2.2 增加微信验证票据解密
 */
@Slf4j
@SuppressWarnings("all")
public class AESCBCUtil {

    public static boolean initialized = false;
    static Charset CHARSET = StandardCharsets.UTF_8;

    /**
     * AES解密
     *
     * @param key   sessionKey key
     * @param iv    iv
     * @param input encryptedData 密文
     * @return 解密后的明文
     */
    public static byte[] decrypt(byte[] key, byte[] iv, byte[] input) {
        initialize();
        try {
            // 对称解密使用的算法为 AES-128-CBC，数据采用PKCS#7填充。
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
            Key sKeySpec = new SecretKeySpec(key, "AES");
            // 初始化
            cipher.init(Cipher.DECRYPT_MODE, sKeySpec, generateIv(iv));
            return cipher.doFinal(input);
        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
            e.printStackTrace();
            log.error("错误的安全模式: {}",e.getMessage());
        } catch (NoSuchPaddingException | IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
            log.error("无效的解密参数: sessionKey: {}, iv: {}, 错误信息: {}",key, iv, e.getMessage());
        } catch (BadPaddingException e) {
            e.printStackTrace();
            log.error("SESSION_KEY 与密文不符,请重新获取 CODE 与 SESSION_KEY");
        } catch (InvalidParameterSpecException e) {
            e.printStackTrace();
            log.error("无效的解密参数长度: sessionKey: {}, iv: {}, 错误信息: {}",key, iv, e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("解密异常: {}",e.getMessage());
        }
        return null;
    }


    /**
     * 对密文进行解密.
     *
     * @param text 需要解密的密文
     * @param appId
     * @param key 解密的key
     * @param encryptedData 密文
     * @return 解密得到的明文
     */
    public static String decrypt(String appId, String key, String encryptedData) {
        final byte[] original;
        final byte[] aesKey = Base64.decodeBase64(key + "=");
        final IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));

        try {
            // 设置解密模式为AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            Key sKeySpec = new SecretKeySpec(aesKey, "AES");
            // 初始化
            cipher.init(Cipher.DECRYPT_MODE, sKeySpec, iv);

            // 使用BASE64对密文进行解码
            byte[] encrypted = Base64.decodeBase64(encryptedData);

            // 解密
            original = cipher.doFinal(encrypted);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("aes解密失败");
        }

        String xmlContent, from_appid;
        try {
            // 去除补位字符
            byte[] bytes = PKCS7Encoder.decode(original);

            // 分离16位随机字符串,网络字节序和AppId
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);

            int xmlLength = recoverNetworkBytesOrder(networkOrder);

            xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
            from_appid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("解密后得到的buffer非法");
        }

        // appid不相同的情况
        if (!from_appid.equals(appId)) {
            throw new RuntimeException("appid校验失败");
        }
        return xmlContent;
    }

    // 还原4个字节的网络字节序
    private static int recoverNetworkBytesOrder(byte[] orderBytes) {
        int sourceNumber = 0;
        for (int i = 0; i < 4; i++) {
            sourceNumber <<= 8;
            sourceNumber |= orderBytes[i] & 0xff;
        }
        return sourceNumber;
    }



    /**
     * 初始化 BouncyCastleProvider, 无需重复构建, 容易内存溢出
     */
    public static void initialize () {
        if (initialized) {
            return;
        }
        synchronized (AESCBCUtil.class) {
            Security.addProvider(new BouncyCastleProvider());
            initialized = true;
        }
    }

    /**
     * 生成iv
     */
    public static AlgorithmParameters generateIv(byte[] iv) throws Exception {
        AlgorithmParameters params = AlgorithmParameters.getInstance("AES");
        params.init(new IvParameterSpec(iv));
        return params;
    }
}
